﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using McMaster.Extensions.CommandLineUtils;

namespace ETTools
{
    internal class OpcodeInfo
    {
        public string Name;
        public int Opcode;
    }
    public class Program
    {
        public static void Main(string[] args)
            => CommandLineApplication.Execute<Program>(args);
        
        [Option("-i|--protoPath", "proto目录,绝对路径要带\"\"",CommandOptionType.SingleValue)]
		public string ProtoDir { get; set; } = "./Proto";

		[Option("-o|--outPath", "输出目录,绝对路径要带\"\"",CommandOptionType.SingleValue)]
		public string OutDir { get; set; } = "./Msg";

        [Option("-f|--ProtoFile","proto文件名",CommandOptionType.SingleValue)]
		public string protoName { get; set; }

        [Option("-s|--NameSpace","指令命名空间",CommandOptionType.SingleValue)]
		public string nameSpace { get; set; }

        [Option("-c|--OpcodeClassName", "指令类名",CommandOptionType.SingleValue)]
		public string opcodeClassName { get; set; }

        [Option("-n|--StartOpcode", "指令起始编号",CommandOptionType.SingleValue)]
		public int startOpcode { get; set; } = 0;

        [Option("-k|--InnerGen", "是否内网协议",CommandOptionType.SingleValue)]
		public int InnerGen { get; set; } = 0;

        private void OnExecute()
        {
            string basePath = ProcessHelper.basePath;
            Console.WriteLine($"AppPath........................{basePath} ");
            Console.WriteLine($"Start Proto2CS........................ProtoDir:{ProtoDir} ");
            Console.WriteLine($"OutDir........................OutDir:{OutDir} ");

            // 命令行输入时，相对路径win用 .\ linux用./，绝对路径要带 ""
            string protoPath,outPath;
            protoPath = ProcessHelper.MakeRelative(basePath,ProtoDir);
            outPath = ProcessHelper.MakeRelative(basePath,OutDir);
            
            // 生成et的内网协议，内网协议不使用google.protobuf数据类型
            if(InnerGen == 1){
                InnerProto2CS.Proto2CS(nameSpace,opcodeClassName,protoName,protoPath,outPath,startOpcode); 
                return;
            }
            
            // 生成消息指令
            // 比如，外网起始编号100，内网起始编号2000，热更协议10000
            if(startOpcode>0)
            {
                if(nameSpace==null||opcodeClassName==null){
                   throw new Exception("生成指令必须指定命名空间与指令类名");
                }
                Proto2Opcode(nameSpace,opcodeClassName, protoName, outPath, 
                    protoPath,startOpcode);
                Console.WriteLine($"{protoName} file :GenerateOpcode succeed!");
                return;
            }

            // 生成消息对象
            if(protoName!=null) {
                Proto2CS(outPath,protoPath,protoName);
                Console.WriteLine($"{protoName} file :proto2cs succeed!");
                return;
            }

            // 未指定proto文件，遍历proto目录
            DirectoryInfo direcinfo = new DirectoryInfo(protoPath);
            if (direcinfo != null && direcinfo.Exists)
            {
                foreach (var item in direcinfo.GetFiles())
                {
                    Console.WriteLine($"protoFile........................{item} ");
                    Proto2CS(outPath,protoPath,item.Name);
                }
            }
            Console.WriteLine($"{protoPath} dir:proto2cs succeed!");
        }


        // 由proto文档生成消息协议的方法
        private static void Proto2CS(string outPath,string protoPath,string protoName){
            string protoc = "";
            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
                protoc = "protoc.exe";
            else protoc = "protoc";
            string filePath = $"{protoPath}/{protoName}";
            ProcessHelper.Run(protoc, $"-I={protoPath} --csharp_out={outPath} {filePath}", waitExit: true);
            
        }

        private static readonly char[] splitChars = { ' ', '\t' };
        private static readonly List<OpcodeInfo> msgOpcode = new List<OpcodeInfo>();

        // 由proto文档生成消息指令的方法
        public static void Proto2Opcode(string ns, string opcodeClassName, string protoName, string outputPath, string protoDir,int startOpcode)
        {
            msgOpcode.Clear();
            string proto = $"{protoDir}/{protoName}";

            string s = File.ReadAllText(proto);

            StringBuilder sb = new StringBuilder();
            sb.Append("using ETModel;\n"); // 如果非et项目在这里修改需要使用的命名空间
            sb.Append($"namespace {ns}\n");
            sb.Append("{\n");

            bool isMsgStart = false;

            foreach (string line in s.Split('\n'))
            {
                string newline = line.Trim();

                if (newline == "")
                {
                    continue;
                }

                if (newline.StartsWith("//"))
                {
                    sb.Append($"{newline}\n");
                }

                if (newline.StartsWith("message"))
                {
                    string parentClass = "";
                    isMsgStart = true;
                    string msgName = newline.Split(splitChars, StringSplitOptions.RemoveEmptyEntries)[1];
                    string[] ss = newline.Split(new[] { "//" }, StringSplitOptions.RemoveEmptyEntries);

                    if (ss.Length == 2)
                    {
                        parentClass = ss[1].Trim();
                    }
                    else
                    {
                        parentClass = "";
                    }

                    msgOpcode.Add(new OpcodeInfo() { Name = msgName, Opcode = ++startOpcode });

                    sb.Append($"\t[Message({opcodeClassName}.{msgName})]\n");
                    sb.Append($"\tpublic partial class {msgName} ");
                    if (parentClass != "")
                    {
                        sb.Append($": {parentClass} ");
                    }

                    sb.Append("{}\n\n");
                }

                if (isMsgStart && newline == "}")
                {
                    isMsgStart = false;
                }
            }

            sb.Append("}\n");

            GenerateOpcode(ns, opcodeClassName, outputPath, sb);
        }

        private static void GenerateOpcode(string ns, string outputFileName, string outputPath, StringBuilder sb)
        {
            sb.AppendLine($"namespace {ns}");
            sb.AppendLine("{");
            sb.AppendLine($"\tpublic static partial class {outputFileName}");
            sb.AppendLine("\t{");
            foreach (OpcodeInfo info in msgOpcode)
            {
                sb.AppendLine($"\t\t public const ushort {info.Name} = {info.Opcode};");
            }

            sb.AppendLine("\t}");
            sb.AppendLine("}");

            string csPath = Path.Combine(outputPath, outputFileName + ".cs");
            File.WriteAllText(csPath, sb.ToString());
        }
    }

    public static class InnerProto2CS
    {
        private static string proto = ".";
        private static readonly char[] splitChars = { ' ', '\t' };
        private static readonly List<OpcodeInfo> msgOpcode = new List<OpcodeInfo>();

        public static void Proto2CS(string ns, string opcodeClassName,string protoName,
                    string ProtoPath,string OutPath,int startOpcode)
        {
            proto = $"{ProtoPath}/{protoName}";
            
            msgOpcode.Clear();
            Proto2CS(ns, protoName, OutPath, opcodeClassName, startOpcode);
            GenerateOpcode(ns, opcodeClassName, OutPath);
        }

        public static void Proto2CS(string ns, string protoName, string outputPath, string opcodeClassName, int startOpcode)
        {
            msgOpcode.Clear();
            
            string csPath = Path.Combine(outputPath, Path.GetFileNameWithoutExtension(proto) + ".cs");

            string s = File.ReadAllText(proto);

            StringBuilder sb = new StringBuilder();
            sb.Append("using ETModel;\n");
            sb.Append("using System.Collections.Generic;\n");
            sb.Append($"namespace {ns}\n");
            sb.Append("{\n");

            bool isMsgStart = false;
            string parentClass = "";
            foreach (string line in s.Split('\n'))
            {
                string newline = line.Trim();

                if (newline == "")
                {
                    continue;
                }

                if (newline.StartsWith("//"))
                {
                    sb.Append($"{newline}\n");
                }

                if (newline.StartsWith("message"))
                {
                    parentClass = "";
                    isMsgStart = true;
                    string msgName = newline.Split(splitChars, StringSplitOptions.RemoveEmptyEntries)[1];
                    string[] ss = newline.Split(new[] { "//" }, StringSplitOptions.RemoveEmptyEntries);

                    if (ss.Length == 2)
                    {
                        parentClass = ss[1].Trim();
                    }

                    msgOpcode.Add(new OpcodeInfo() { Name = msgName, Opcode = ++startOpcode });

                    sb.Append($"\t[Message({opcodeClassName}.{msgName})]\n");
                    sb.Append($"\tpublic partial class {msgName}");
                    if (parentClass == "IActorMessage" || parentClass == "IActorRequest" || parentClass == "IActorResponse" ||
                        parentClass == "IFrameMessage")
                    {
                        sb.Append($": {parentClass}\n");
                    }
                    else if (parentClass != "")
                    {
                        sb.Append($": {parentClass}\n");
                    }
                    else
                    {
                        sb.Append("\n");
                    }

                    continue;
                }

                if (isMsgStart)
                {
                    if (newline == "{")
                    {
                        sb.Append("\t{\n");
                        continue;
                    }

                    if (newline == "}")
                    {
                        isMsgStart = false;
                        sb.Append("\t}\n\n");
                        continue;
                    }

                    if (newline.Trim().StartsWith("//"))
                    {
                        sb.AppendLine(newline);
                        continue;
                    }

                    if (newline.Trim() != "" && newline != "}")
                    {
                        if (newline.StartsWith("repeated"))
                        {
                            Repeated(sb, ns, newline);
                        }
                        else
                        {
                            Members(sb, newline, true);
                        }
                    }
                }
            }

            sb.Append("}\n");

            File.WriteAllText(csPath, sb.ToString());
        }

        private static void GenerateOpcode(string ns, string outputFileName, string outputPath)
        {
            StringBuilder sb = new StringBuilder();
            sb.AppendLine($"namespace {ns}");
            sb.AppendLine("{");
            sb.AppendLine($"\tpublic static partial class {outputFileName}");
            sb.AppendLine("\t{");
            foreach (OpcodeInfo info in msgOpcode)
            {
                sb.AppendLine($"\t\t public const ushort {info.Name} = {info.Opcode};");
            }

            sb.AppendLine("\t}");
            sb.AppendLine("}");

            string csPath = Path.Combine(outputPath, outputFileName + ".cs");
            File.WriteAllText(csPath, sb.ToString());
        }

        private static void Repeated(StringBuilder sb, string ns, string newline)
        {
            try
            {
                int index = newline.IndexOf(";");
                newline = newline.Remove(index);
                string[] ss = newline.Split(splitChars, StringSplitOptions.RemoveEmptyEntries);
                string type = ss[1];
                type = ConvertType(type);
                string name = ss[2];

                sb.Append($"\t\tpublic List<{type}> {name} = new List<{type}>();\n\n");
            }
            catch (Exception e)
            {
                Console.WriteLine($"{newline}\n {e}");
            }
        }

        private static string ConvertType(string type)
        {
            string typeCs = "";
            switch (type)
            {
                case "int16":
                    typeCs = "short";
                    break;
                case "int32":
                    typeCs = "int";
                    break;
                case "bytes":
                    typeCs = "byte[]";
                    break;
                case "uint32":
                    typeCs = "uint";
                    break;
                case "long":
                    typeCs = "long";
                    break;
                case "int64":
                    typeCs = "long";
                    break;
                case "uint64":
                    typeCs = "ulong";
                    break;
                case "uint16":
                    typeCs = "ushort";
                    break;
                default:
                    typeCs = type;
                    break;
            }

            return typeCs;
        }

        private static void Members(StringBuilder sb, string newline, bool isRequired)
        {
            try
            {
                int index = newline.IndexOf(";");
                newline = newline.Remove(index);
                string[] ss = newline.Split(splitChars, StringSplitOptions.RemoveEmptyEntries);
                string type = ss[0];
                string name = ss[1];
                string typeCs = ConvertType(type);

                sb.Append($"\t\tpublic {typeCs} {name} {{ get; set; }}\n\n");
            }
            catch (Exception e)
            {
                Console.WriteLine($"{newline}\n {e}");
            }
        }
    }
}
